﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Enyim.Caching;
using Enyim.Caching.Memcached;

namespace memcached
{
    /// <summary>
    /// Memcached store client.
    /// Not thread safe, but safe once initialized.
    /// </summary>
    public class MemCache : ICacheAdvanced, IDisposable
    {
        #region Housekeeping
        private MemcachedClient client;
        private bool owned;

        private void Initialize()
        {
            System.Diagnostics.Debug.Assert(client == null);
            client = new MemcachedClient("enyim.com/memcached");
        }

        /// <summary>
        /// Initialize new memcached store client.
        /// </summary>
        /// <remarks>The config section has to be named "enyim.com/memcached"</remarks>
        [System.Diagnostics.DebuggerHidden]
        public MemCache() { }

        /// <summary>
        /// Initialize with a provided client.
        /// </summary>
        /// <param name="client">memcached store client</param>
        public MemCache(MemcachedClient client)
        {
            if (client == null)
                throw new ArgumentNullException("client");
            this.client = client;
        }

        /// <summary>
        /// Initialize with provided endpoint and key transformer.
        /// </summary>
        /// <param name="address">not null</param>
        /// <param name="port">not null</param>
        /// <param name="maxPoolSize">positive</param>
        /// <param name="keyTransformer">not null</param>
        public MemCache(System.Net.IPAddress address, int port, int maxPoolSize, Type keyTransformer)
        {
            var c = new Enyim.Caching.Configuration.MemcachedClientConfiguration();
            c.KeyTransformer = keyTransformer;
            c.NodeLocator = typeof(SingleNodeLocator);
            c.Transcoder = typeof(DefaultTranscoder);
            c.Servers.Add(new System.Net.IPEndPoint(address, port));
            c.SocketPool.MinPoolSize = 1;
            c.SocketPool.MaxPoolSize = maxPoolSize;
            this.client = new MemcachedClient(c);
        }

        #region Internal helpers
        [System.Diagnostics.DebuggerHidden]
        internal MemCache(bool owned) { this.owned = owned; }

        [System.Diagnostics.DebuggerHidden]
        internal void DisposeInternal()
        {
            owned = false;
            Dispose();
        }
        #endregion

        /// <summary>
        /// </summary>
        public void Dispose()
        {
            GC.SuppressFinalize(this);
            if (client != null)
            {
                if (owned)
                    throw new InvalidOperationException("You should not Dispose this instance as you didn't create it.");
                client.Dispose();
                client = null;
            }
        }
        #endregion

        #region Operations
        /// <summary>
        /// Add 1 to the key value or create a new one.
        /// </summary>
        /// <param name="key"></param>
        /// <param name="startAt">initial value to set to</param>
        /// <returns>new value of the item</returns>
        public long Increment(string key, long startAt)
        {
            if (startAt < 0)
                throw new ArgumentOutOfRangeException("startAt");
            if (client == null)
                Initialize();
            long val = client.Increment(key, 1);
            if (val == -1)
            {
                Set(key, startAt.ToString());// due to an API error, we need string
                val = startAt;
            }
            return val;
        }

        /// <summary>
        /// Subtract 1 from the key value. Will not subtract beyond 0.
        /// </summary>
        /// <param name="key"></param>
        /// <returns>-1 if item not found, 0 if value was &lt;1</returns>
        public long Decrement(string key)
        {
            if (client == null)
                Initialize();
            long val = client.Decrement(key, 1);
            return val;
        }

        /// <summary></summary>
        public bool Remove(string key)
        {
            if (client == null)
                Initialize();
            return client.Remove(key);
        }

        #region Set
        /// <summary></summary>
        public bool Set(string key, object value)
        {
            if (client == null)
                Initialize();
            return client.Store(StoreMode.Set, key, value);
        }

        /// <summary>
        /// Set value to expire after N seconds.
        /// </summary>
        /// <param name="key"></param>
        /// <param name="validFor">in seconds</param>
        /// <param name="value"></param>
        /// <returns></returns>
        public bool Set(string key, int validFor, object value)
        {
            if (client == null)
                Initialize();
            return client.Store(StoreMode.Set, key, value, TimeSpan.FromSeconds(validFor));
        }

        /// <summary></summary>
        public bool Set(string key, DateTime expireAt, object value)
        {
            if (client == null)
                Initialize();
            return client.Store(StoreMode.Set, key, value, expireAt);
        }

        /// <summary></summary>
        public bool Set(string key, ulong cas, object value)
        {
            if (client == null)
                Initialize();
            return client.CheckAndSet(key, value, cas);
        }
        #endregion

        #region Add
        /// <summary></summary>
        public bool Add(string key, int validFor, object value)
        {
            if (client == null)
                Initialize();
            // add is atomic, will return false if item exists already
            return client.Store(StoreMode.Add, key, value, TimeSpan.FromSeconds(validFor));
        }

        /// <summary></summary>
        public bool Add(string key, DateTime expireAt, object value)
        {
            if (client == null)
                Initialize();
            // add is atomic, will return false if item exists already
            return client.Store(StoreMode.Add, key, value, expireAt);
        }
        #endregion

        #region Get
        /// <summary></summary>
        public T Get<T>(string key, out bool success)
        {
            if (client == null)
                Initialize();
            try
            {
                object v = client.Get(key, out success);
                if (v == null || !(v is T))
                    return default(T);
                return (T)v;
            }
            catch (MemcachedClientException e)
            {
                System.Diagnostics.Trace.TraceError(e.Message);
                success = false;
                return default(T);
            }
        }

        /// <summary></summary>
        public object Get(string key, out bool success)
        {
            if (client == null)
                Initialize();
            try
            {
                return client.Get(key, out success);
            }
            catch (MemcachedClientException e)
            {
                System.Diagnostics.Trace.TraceError(e.Message);
                success = false;
                return null;
            }
        }

        /// <summary></summary>
        public CasValue<T> Get<T>(string key)
        {
            if (client == null)
                Initialize();
            string[] keys = new string[1] { key };
            IDictionary<string, ulong> cas;
            IDictionary<string, object> res = client.Get(keys, out cas);
            if (res != null && res.ContainsKey(key))
            {
                System.Diagnostics.Debug.Assert(cas != null);
                System.Diagnostics.Debug.Assert(cas.ContainsKey(key));
                object v = res[key];
                if (v is T)
                    return new CasValue<T>((T)v, cas[key]);
            }
            return null;
        }
        #endregion
        #endregion
    }
}